# -*- coding: utf-8 -*- 
# # # # # # # # # # # # # # # # # # # # #
#                                       #                                       
#    Trabajo Práctico 3 - Conexiones    # 
#                                       #
#     Teoría de las Comunicaciones      #
#      Departamento de Computación      #
#              FCEN - UBA               #
#            junio de 2013              #
#                                       #
# # # # # # # # # # # # # # # # # # # # #


import threading
import random
import time

from common import PacketBuilder, ProtocolControlBlock
from soquete import Soquete
from packet import ACKFlag, FINFlag, SYNFlag
from worker import ClientProtocolWorker
from buffers import DataBuffer, RetransmissionQueue, NotEnoughDataException
from constants import MIN_PACKET_SIZE, MAX_PACKET_SIZE, CLOSED,\
                      ESTABLISHED, FIN_SENT, SYN_SENT, MAX_SEQ,\
                      SEND_WINDOW, MAX_RETRANSMISSION_ATTEMPTS


class ClientControlBlock(ProtocolControlBlock):
    
    def __init__(self, address, port):
        ProtocolControlBlock.__init__(self, address, port)
        # Próximo SEQ a enviar
        self.send_seq = random.randint(1, MAX_SEQ)
        # Tamaño de la ventana de emisión
        self.send_window = SEND_WINDOW
        # Límite inferior de la ventana (i.e., unacknowledged)
        self.window_lo = self.send_seq
        # Límite superior de la ventana
        self.window_hi = self.modular_sum(self.window_lo, self.send_window)
        
    def get_send_seq(self):
        return self.send_seq
    
    def get_send_window(self):
        return self.send_window
        
    
    #############################
    ### Completar esta clase! ###
    #############################

	#implementados por nosotros    
    
    # Responde True sii la ventana de emisión no está saturada.
    def send_allowed(self):
        return self.send_seq % SEND_WINDOW <= self.window_hi
        
	#devuelve True si el ack_number es correcto
    def accepts(self, ack_number):
        
        if (self.window_hi < self.window_lo):
            doAccept = ack_number >= self.window_lo or ack_number <= self.window_hi
        else:
            doAccept = self.window_lo <= ack_number <= self.window_hi
            
        return doAccept    	
        
    # Incrementa #SEQ en uno modulo sobre el tamaño de la ventana de emision   
    def increment_send_seq(self):
        self.send_seq = self.modular_increment(self.send_seq)         
        
    def update_sliding_window(self, packet):
        self.window_lo = self.modular_increment(packet.get_ack_number()) 
        self.window_hi = self.modular_sum(self.window_lo, self.send_window)

        
    #fin implementados por nosotros
        

class PTCClientProtocol(object):
    
    def __init__(self, address, port):
        self.retransmission_queue = RetransmissionQueue(self)
        self.retransmission_attempts = dict()
        self.outgoing_buffer = DataBuffer()
        self.state = CLOSED
        self.control_block = ClientControlBlock(address, port)
        self.socket = Soquete(address, port)
        self.packet_builder = PacketBuilder(self)
    
    def is_connected(self):
        return self.state == ESTABLISHED
        
    def build_packet(self, payload=None, flags=None):
        seq = self.control_block.get_send_seq()
        if payload is not None:
            self.control_block.increment_send_seq()
        packet = self.packet_builder.build(payload=payload, flags=flags, seq=seq)
        return packet
        
    def send_packet(self, packet):
        self.socket.send(packet)
        
    def send_and_queue_packet(self, packet):
        self.send_packet(packet)
        self.retransmission_queue.put(packet)
        
    def send(self, data):
        if not self.is_connected():
            raise Exception('cannot send data: connection not established')
        self.worker.send(data)

    def sendKbNtimes(self, filename, repeat):  

        if not self.is_connected():
            raise Exception('cannot send data: connection not established')

        with open(filename, 'r') as f:
            fileContent = f.read()
            
        acum = 0
        counter = repeat               
        while counter > 0:
            counter -= 1    
            start_time = time.time()            
            self.worker.send(fileContent)
            acum += time.time() - start_time
       
        print acum / repeat , "seconds"       

        
    def connect_to(self, address, port):
        self.worker = ClientProtocolWorker.spawn_for(self)
        self.worker.start()
        self.connected_event = threading.Event()
        self.control_block.set_destination_address(address)
        self.control_block.set_destination_port(port)
        
        syn_packet = self.build_packet(flags=[SYNFlag])
        self.send_and_queue_packet(syn_packet)
        self.state = SYN_SENT
        
        self.connected_event.wait()
    
    def handle_timeout(self):
        
        new_retransmission_queue = RetransmissionQueue(self)
        # (1) Obtener los paquetes en self.retranmission_queue
        for packet in self.retransmission_queue:
        # ...y verificar que no se exceda la cantidad máxima de reenvíos!
            if self.isRetransmissionExceeded(packet):
                # (hacer self.shutdown() si esto ocurre y dejar un mensaje en self.error)
                self.shutdown()
                self.error = 'packet exceeded Max Retransmission Attempts'
            else:
                # (2) Volver a enviarlos
                self.send_retransmission(packet)
                # (3) Reencolarlos para otra eventual retransmisión                
                new_retransmission_queue.put(packet)
                # Actualizar contador de intentos
                self.updateRetransmissionAttempts(packet)
            
        self.retransmission_queue = new_retransmission_queue

        
    def isRetransmissionExceeded(self, packet):
        return self.retransmission_attempts.get(packet) == MAX_RETRANSMISSION_ATTEMPTS
      
    def updateRetransmissionAttempts(self, packet):
        value = 1
        if (self.retransmission_attempts.has_key(packet)):
            value += self.retransmission_attempts.pop(packet)
            
        self.retransmission_attempts[packet] = value            
        
    def handle_pending_data(self):
        more_data_pending = False
        
        if self.control_block.send_allowed():
            try:
                data = self.outgoing_buffer.get(MIN_PACKET_SIZE, MAX_PACKET_SIZE)
            except NotEnoughDataException:
                pass
            else:
                packet = self.build_packet(payload=data)
                self.send_and_queue_packet(packet)
                
            if not self.outgoing_buffer.empty():
                more_data_pending = True
        else:
            more_data_pending = True
        
        if more_data_pending:
            self.worker.signal_pending_data()
    

    def handle_incoming(self, packet):
        # * Corroborar que el flag de ACK esté seteado y
        # * Analizar si #ACK es aceptado (hablar con el bloque de control para hacer este checkeo) 
        if self.isValidACK(packet):
            if self.state == SYN_SENT:
                # * Caso donde el estado es SYN_SENT esperando por aceptacion de conexion
                self.state = ESTABLISHED
                # Para este caso se incrementa el send seq number
                self.control_block.increment_send_seq()
                # * No olvidar de hacer self.connected_event.set() al confirmar el ACK y establecer la conexión!!!
                self.connected_event.set()
            elif self.state == FIN_SENT:
                # * Caso donde el estado es FIN_SENT
                self.state = CLOSED
            # * Sacar de la cola de retransmisión los paquetes reconocidos por #ACK
            self.retransmission_queue.acknowledge(packet)
            # * Ajustar la ventana deslizante con #ACK
            self.control_block.update_sliding_window(packet)             
      
    def isValidACK(self,packet):
        return ACKFlag in packet and self.control_block.accepts(packet.get_ack_number())
            
    def send_retransmission(self, packet):
        self.send_packet(packet);
                
    def handle_close_connection(self):
        if not self.outgoing_buffer.empty():
            self.worker.signal_pending_data()
            self.worker.signal_close_connection()
        elif not self.retransmission_queue.empty():
            self.worker.signal_close_connection()
        else:
            fin_packet = self.build_packet(flags=[FINFlag])
            self.send_and_queue_packet(fin_packet)
            self.state = FIN_SENT
        
    def close(self):
        if self.is_connected():
            self.worker.signal_close_connection()
        
    def shutdown(self):
        self.outgoing_buffer.clear()
        self.retransmission_queue.clear()
        self.retransmission_attempts.clear()
        self.worker.stop()
        # Esto es por si falló el establecimiento de conexión (para destrabar al thread principal)
        self.connected_event.set()
        self.state = CLOSED